package com.starview.star.compiler.cafe.compile;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.starview.platform.data.IValue;
import com.starview.platform.data.type.Location;
import com.starview.star.code.repository.CodeCatalog;
import com.starview.star.code.repository.CodeRepository;
import com.starview.star.compiler.Display;
import com.starview.star.compiler.ErrorReport;
import com.starview.star.compiler.ast.Abstract;
import com.starview.star.compiler.ast.Apply;
import com.starview.star.compiler.ast.DefaultAbstractVisitor;
import com.starview.star.compiler.ast.IAbstract;
import com.starview.star.compiler.ast.Name;
import com.starview.star.compiler.cafe.CafeSyntax;
import com.starview.star.compiler.util.PrettyPrintDisplay;
import com.starview.star.compiler.util.PrettyPrintable;
import com.starview.star.compiler.util.TopologySort;
import com.starview.star.compiler.util.TopologySort.IDefinition;

public class Dependencies
{

  public static List<List<IAbstract>> dependencySort(CodeRepository repository, List<IAbstract> defs, Location loc,
      CodeCatalog bldCat, ErrorReport errors)
  {
    Set<IAbstract> allList = pickList(repository, defs, loc, bldCat, errors);
    List<ThetaDefn> initGroup = new ArrayList<Dependencies.ThetaDefn>();
    for (IAbstract def : defs)
      initGroup.add(new ThetaDefn(def, pickList(repository, def, loc, bldCat, errors), allList));
    List<List<IDefinition<IAbstract>>> groups = new TopologySort<IAbstract>(initGroup).sort();
    List<List<IAbstract>> sortd = new ArrayList<List<IAbstract>>();
    for (List<IDefinition<IAbstract>> entry : groups) {
      List<IAbstract> group = new ArrayList<IAbstract>();
      for (IDefinition<IAbstract> def : entry)
        group.add(((ThetaDefn) def).defn);
      sortd.add(group);
    }

    return sortd;
  }

  private static Set<IAbstract> pickList(CodeRepository repository, List<IAbstract> definitions, Location loc,
      CodeCatalog bldCat, ErrorReport errors)
  {
    Set<IAbstract> pick = new HashSet<>();
    for (IAbstract def : definitions) {
      defined(repository, loc, bldCat, errors, pick, def);
    }
    return pick;
  }

  private static Set<IAbstract> pickList(CodeRepository repository, IAbstract def, Location loc, CodeCatalog bldCat,
      ErrorReport errors)
  {
    Set<IAbstract> pick = new HashSet<>();

    defined(repository, loc, bldCat, errors, pick, def);

    return pick;
  }

  private static void defined(CodeRepository repository, Location loc, CodeCatalog bldCat, ErrorReport errors,
      Set<IAbstract> pick, IAbstract def)
  {
    if (CafeSyntax.isTypeDef(def)) {
      Name name = new Name(Location.nullLoc, CafeSyntax.typeDefName(def));
      pick.add(name);
    } else if (CafeSyntax.isFunctionDefn(def)) {
      Name name = new Name(Location.nullLoc, CafeSyntax.definedFunctionName(def));
      pick.add(name);
    } else if (CafeSyntax.isVarDeclaration(def)) {
      List<IAbstract> declaredVars = Patterns.declaredVars(CafeSyntax.varDeclLval(def));
      for (IAbstract v : declaredVars)
        pick.add(v);
    } else if (CafeSyntax.isIsDeclaration(def)) {
      List<IAbstract> declaredVars = Patterns.declaredVars(CafeSyntax.isDeclLval(def));
      for (IAbstract v : declaredVars)
        pick.add(v);
    }
  }

  private static Collection<IAbstract> allReferences(IAbstract defn, Set<IAbstract> defs, Set<IAbstract> pickList)
  {
    ReferenceFinder finder = new ReferenceFinder(defs, pickList);
    if (CafeSyntax.isIsDeclaration(defn))
      defn = CafeSyntax.isDeclValue(defn);
    else if (CafeSyntax.isVarDeclaration(defn))
      defn = CafeSyntax.varDeclValue(defn);

    defn.accept(finder);
    return finder.references;
  }

  private static class ReferenceFinder extends DefaultAbstractVisitor
  {
    private final Collection<String> pickList = new HashSet<String>();
    private final Collection<String> defined = new HashSet<String>();
    private final Collection<IAbstract> references = new HashSet<IAbstract>();
    private final Collection<IAbstract> excludes = new HashSet<IAbstract>();

    ReferenceFinder(Set<IAbstract> defined, Set<IAbstract> pickList)
    {
      for (IAbstract d : defined)
        if (d instanceof Name)
          this.defined.add(Abstract.getId(d));
      for (IAbstract d : pickList)
        if (d instanceof Name)
          this.pickList.add(Abstract.getId(d));
    }

    @Override
    public void visitName(Name name)
    {
      String n = name.getId();
      if (pickList.contains(n) && !defined.contains(n) && !references.contains(name) && !excludes.contains(name))
        references.add(name);
    }

    @Override
    public void visitApply(Apply app)
    {
      if (CafeSyntax.isVarDeclaration(app))
        addVarToExcludes(CafeSyntax.varDeclLval(app));
      else if (CafeSyntax.isIsDeclaration(app))
        addVarToExcludes(CafeSyntax.isDeclLval(app));

      if (CafeSyntax.isDot(app))
        CafeSyntax.dotRecord(app).accept(this);
      else if (CafeSyntax.isFunCall(app)) {
        CafeSyntax.funCallOperator(app).accept(this);
        CafeSyntax.funCallArgs(app).accept(this);
      } else if (CafeSyntax.isEscape(app)) {
        for (IValue arg : CafeSyntax.escapeArgs(app))
          ((IAbstract)arg).accept(this);
      } else if (CafeSyntax.isConstructor(app)) {
        CafeSyntax.constructorName(app).accept(this);
        for (IValue arg : CafeSyntax.constructorArgs(app))
          ((IAbstract)arg).accept(this);
      } else if (!CafeSyntax.isImport(app))
        super.visitApply(app);
    }

    private void addVarToExcludes(IAbstract ptn)
    {
      if (CafeSyntax.isTypedTerm(ptn))
        addVarToExcludes(CafeSyntax.typedTerm(ptn));
      else if (ptn instanceof Name && !excludes.contains(ptn))
        excludes.add(ptn);
    }

  }

  @SuppressWarnings("serial")
  private static class ThetaDefn implements IDefinition<IAbstract>, PrettyPrintable
  {
    private final IAbstract defn;

    private final Set<IAbstract> defs;
    private final Collection<IAbstract> references;

    public ThetaDefn(IAbstract defn, Set<IAbstract> defs, Set<IAbstract> allList)
    {
      this.defn = defn;
      this.defs = defs;
      this.references = allReferences(defn, this.defs, allList);
    }

    @Override
    public boolean defines(IAbstract obj)
    {
      for (IAbstract def : defs)
        if (def.equals(obj))
          return true;
      return false;
    }

    @Override
    public Collection<IAbstract> references()
    {
      return references;
    }

    @Override
    public void prettyPrint(PrettyPrintDisplay disp)
    {
      disp.append("[");
      Display.display(disp, defs, ", ");
      disp.append("]:{");
      Display.display(disp, references, ", ");
      disp.append("}");
    }

    @Override
    public String toString()
    {
      return PrettyPrintDisplay.toString(this);
    }
  }
}
